#!/usr/bin/python2 -d
# -*- coding: utf-8 -*-

import os, sys
from errno import *
from stat import *
import fcntl

# Fuse
import fuse
from fuse import Fuse

# File
import RawFileIO
from RawFileIO import MyFileIO

if not hasattr(fuse, '__version__'):
    raise RuntimeError(
        "your fuse-py doesn't know of fuse.__version__, probably it's too old.")

fuse.fuse_python_api = (0, 2)

# We use a custom file class
fuse.feature_assert('stateful_files', 'has_init')

__version__='0.1a'

def flag2mode(flags):
    md = {os.O_RDONLY: 'r', os.O_WRONLY: 'w', os.O_RDWR: 'w+'}
    m = md[flags & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR)]

    if flags & os.O_APPEND:
        m = m.replace('w', 'a', 1)

    return m

def mygetattr(path, ivsize=16):
    st = fuse.Stat()
    if isinstance(path, int):
        origst = os.fstat(path)
    elif isinstance(path, str) or isinstance(path, basestring):
        origst = os.lstat(path)
    size = getattr(origst, 'st_size', 0)
    [setattr(st, i, getattr(origst, i, 0)) for i in ('st_mode',
                                            'st_ino',
                                            'st_dev',
                                            'st_nlink',
                                            'st_uid',
                                            'st_gid',
                                            'st_atime',
                                            'st_mtime',
                                            'st_ctime')
    ]

    st.st_size = 0 if size <= ivsize else size - ivsize
    return st

class MyFuse(Fuse):
    def __init__(self, *args, **kw):
        Fuse.__init__(self, *args, **kw)

        # do stuff to set up your filesystem here, if you want
        #import thread
        #thread.start_new_thread(self.mythread, ())
        #self.root = '/home/mann/.myfuse'

#    def mythread(self):
#
#        """
#        The beauty of the FUSE python implementation is that with the python interp
#        running in foreground, you can have threads
#        """
#        print "mythread: started"
#        while 1:
#            time.sleep(120)
#            print "mythread: ticking"


    def getattr(self, path):
        if os.path.isdir('.' + path):
            st = os.lstat('.' + path)
        else:
            m = self.MyFile('.' + path, os.O_RDWR)
            st = m.fgetattr()
        return st
        #return os.lstat(path)

    def readlink(self, path):
        # readlink is not supported
        return -1

    def readdir(self, path, offset):
        yield fuse.Direntry('.')
        yield fuse.Direntry('..')
        for e in os.listdir('.' + path):
            yield fuse.Direntry(e)

    def unlink(self, path):
        print 'unlink', '.' + path
        os.unlink('.' + path)

    def rmdir(self, path):
        os.rmdir('.' + path)

    def symlink(self, path, path1):
        # symlink is not supported
        print >> sys.stdout, 'symlink is not supported'
        return -1

    def rename(self, path, path1):
        os.rename('.' + path, '.' + path1)

    def link(self, path, path1):
        # link is not supported
        print >> sys.stdout, 'link is not supported'
        return -1

    def chmod(self, path, mode):
        os.chmod('.' + path, mode)

    def chown(self, path, user, group):
        os.chown('.' + path, user, group)

    def mkdir(self, path, mode):
        os.mkdir('.' + path, mode)

    def mknod(self, path, mode, dev):
        os.mknod("." + path, mode, dev)

    def utime(self, path, times):
        os.utime('.' + path, times)

# DO NOT IMPLEMENTED
#    The following utimens method would do the same as the above utime method.
#    We can't make it better though as the Python stdlib doesn't know of
#    subsecond preciseness in acces/modify times.
#
#    def utimens(self, path, ts_acc, ts_mod):
#      os.utime("." + path, (ts_acc.tv_sec, ts_mod.tv_sec))

    def access(self, path, mode):
        if not os.access('.' + path, mode):
            return -EACCES

# DO NOT IMPLEMENTED
#    This is how we could add stub extended attribute handlers...
#    (We can't have ones which aptly delegate requests to the underlying fs
#    because Python lacks a standard xattr interface.)
#
#    def getxattr(self, path, name, size):
#        val = name.swapcase() + '@' + path
#        if size == 0:
#            # We are asked for size of the value.
#            return len(val)
#        return val
#
#    def listxattr(self, path, size):
#        # We use the "user" namespace to please XFS utils
#        aa = ["user." + a for a in ("foo", "bar")]
#        if size == 0:
#            # We are asked for size of the attr list, ie. joint size of attrs
#            # plus null separators.
#            return len("".join(aa)) + len(aa)
#        return aa


    def statfs(self):
        """
        Should return an object with statvfs attributes (f_bsize, f_frsize...).
        Eg., the return value of os.statvfs() is such a thing (since py 2.2).
        If you are not reusing an existing statvfs object, start with
        fuse.StatVFS(), and define the attributes.

        To provide usable information (ie., you want sensible df(1)
        output, you are suggested to specify the following attributes:

            - f_bsize - preferred size of file blocks, in bytes
            - f_frsize - fundamental size of file blcoks, in bytes
                [if you have no idea, use the same as blocksize]
            - f_blocks - total number of blocks in the filesystem
            - f_bfree - number of free blocks
            - f_files - total number of file inodes
            - f_ffree - nunber of free file inodes
        """
        return os.statvfs(self.root)

    def truncate(self, path, len):
        self.MyFile(path, os.O_RDWR).ftruncate(len)

    def fsinit(self):
        os.chdir(self.root)

    def main(self, *a, **kw):
        self.file_class = self.MyFile

        # params
        params = self.cmdline[0]
        [setattr(self.file_class,
                i,
                getattr(self, i, None)) for i in ('password',
                                                    'login',
                                                    'cipher',
                                                    'ciphermode',
                                                    'blocksize',
                                                    'keysize')
        ]
        return Fuse.main(self, *a, **kw)

    class MyFile(MyFileIO):
        def __init__(self, path, flags, *mode):
            [setattr(self,
                     i,
                     getattr(self,
                            i,
                            RawFileIO.getdefault(i))) for i in ('cipher',
                                                                'ciphermode',
                                                                'keysize',
                                                                'blocksize',
                                                                'password',
                                                                'login')
            ]

            if flags & os.O_APPEND:
                flags = flags - os.O_APPEND | os.O_RDWR
            # всегда должна оставаться возможность чтения
            # заменить os.O_RDONLY на os.O_RDWR
            if flags & os.O_WRONLY:
                flags = flags - os.O_WRONLY | os.O_RDWR
            # не удается открыть файл для чтения при открытии для записи
            print 'flags',  flags, flag2mode(flags)

            path = RawFileIO.getPath(path)
            self.fd = os.open(path, flags, *mode)
            self.file = os.fdopen(self.fd, flag2mode(flags))

            print 'MyFile',  path, self.fd, flags,  mode

            self.cryptclass = MyFileIO(self.fd, flags, *mode,
                                    blocksize=self.blocksize,
                                    keysize=self.keysize,
                                    cipher=self.cipher,
                                    ciphermode=self.ciphermode,
                                    login=self.login,
                                    password=self.password)

        def read (self,  length, offset):
            return self.cryptclass.read(length, offset)

        def ftruncate(self, len):
            return self.cryptclass.ftruncate(len)

        def write(self,  buf, offset):
            return self.cryptclass.write(buf, offset)

        def _fflush(self):
            if 'w' in self.file.mode or 'a' in self.file.mode:
                self.file.flush()

        def flush(self):
            self._fflush()
            # cf. xmp_flush() in fusexmp_fh.c
            os.close(os.dup(self.fd))

        def release(self, flags):
            os.close(self.fd)

        def fsync(self, isfsyncfile):
            self._fflush()
            if isfsyncfile and hasattr(os, 'fdatasync'):
                os.fdatasync(self.fd)
            else:
                os.fsync(self.fd)

        def fgetattr(self):
            return mygetattr(self.fd,
                        ivsize=self.cryptclass.ivsize)

        def lock(self, cmd, owner, **kw):
            print 'lock',  cmd,  owner,  kw
            # The code here is much rather just a demonstration of the locking
            # API than something which actually was seen to be useful.

            # Advisory file locking is pretty messy in Unix, and the Python
            # interface to this doesn't make it better.
            # We can't do fcntl(2)/F_GETLK from Python in a platfrom independent
            # way. The following implementation *might* work under Linux.
            #
            # if cmd == fcntl.F_GETLK:
            #     import struct
            #
            #     lockdata = struct.pack('hhQQi', kw['l_type'], os.SEEK_SET,
            #                            kw['l_start'], kw['l_len'], kw['l_pid'])
            #     ld2 = fcntl.fcntl(self.fd, fcntl.F_GETLK, lockdata)
            #     flockfields = ('l_type', 'l_whence', 'l_start', 'l_len', 'l_pid')
            #     uld2 = struct.unpack('hhQQi', ld2)
            #     res = {}
            #     for i in xrange(len(uld2)):
            #          res[flockfields[i]] = uld2[i]
            #
            #     return fuse.Flock(**res)

            # Convert fcntl-ish lock parameters to Python's weird
            # lockf(3)/flock(2) medley locking API...
            op = { fcntl.F_UNLCK : fcntl.LOCK_UN,
                   fcntl.F_RDLCK : fcntl.LOCK_SH,
                   fcntl.F_WRLCK : fcntl.LOCK_EX }[kw['l_type']]
            if cmd == fcntl.F_GETLK:
                return -EOPNOTSUPP
            elif cmd == fcntl.F_SETLK:
                if op != fcntl.LOCK_UN:
                    op |= fcntl.LOCK_NB
            elif cmd == fcntl.F_SETLKW:
                pass
            else:
                return -EINVAL

            fcntl.lockf(self.fd, op, kw['l_start'], kw['l_len'])


def main():

    usage = '''
PyEncryptFS is a program which provides an encrypted virtual filesystem for Linux using the FUSE kernel module.\n
''' + Fuse.fusage

    #server = MyFuse(version="%prog " + fuse.__version__,
    #                usage=usage, dash_s_do='setsingle')
    server = MyFuse(version="%prog " + __version__,
                    usage=usage)

    PARSERVARS = [{'mountopt': 'root', 'metavar': 'PATH', 'default': None,
                'help': 'mirror filesystem from under PATH [default: %default]'},
           {'mountopt': 'login', 'metavar': 'alex@mail.com', 'default': '',
                'help': 'Login. Usally Email'},
           {'mountopt': 'password', 'metavar': 'PASSWORD', 'default': '',
                'help': 'Your password'},
           {'mountopt': 'cipher', 'metavar': 'CIPHER', 'default': 'AES',
                'help': 'Cipher algorithm. Use AES or Blowfish'},
           {'mountopt': 'ciphermode', 'metavar': 'CIPHERMODE', 'default': 'CFB',
                'help': 'Cipher algorithm mode. Use CFB or PGP modes'},
           {'mountopt': 'keysize', 'metavar': 'KEYSIZE', 'default': '128',
                'help': 'Cipher key size. Use 128 or 256 key bits'},
           {'mountopt': 'blocksize', 'metavar': 'BLOCKSIZE', 'default': '4096',
                'help': 'Cipher block size. Use block size in 512, 1024, 4*1024, 16*1024, 64*1024 bits'}
                ]

    [server.parser.add_option(mountopt=i['mountopt'],
                            metavar=i['metavar'],
                            default=i['default'],
                            help=i['help']) for i in PARSERVARS]
    server.parse(values=server, errex=1)

    if len(sys.argv) < 2:
        sys.stderr.write('Can`t mount. Not listed the main parameters!\n\n')
        server.parser.print_usage()
        sys.exit(1)

    #if not (hasattr(server, 'root') and hasattr(server, 'password')):
    #    sys.stderr.write('Can`t mount. Not listed the main parameters!\nUse -h|--help option to get help.\n')
    #    sys.exit(1)

    try:
        if server.fuse_args.mount_expected():
            os.chdir(server.root)
    except:
        sys.stderr.write("Can't enter root of underlying filesystem\n\n")
        server.parser.print_usage()
        sys.exit(1)
    server.main()

if __name__ == '__main__':
    main()
